﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace IndexedFilePackageLibrary
{
    public class IndexedFilePackageBuilder
    {
        private const int _FileHeaderSize = 16;

        private readonly Dictionary<string, FileNode> _nodes = new Dictionary<string, FileNode>();
        private int _count = 0;

        public string BasePath { get; set; }

        public short MaxItemCountPerNode { get; set; } = 7;
        public short IndexNodePadding { get; set; } = 4;
        public byte FileNodePadding { get; set; } = 4;

        public bool UseIntFileSize { get; set; }

        public IndexedFilePackageBuilder() : this(null) { }
        public IndexedFilePackageBuilder(string basePath)
        {
            BasePath = basePath;
        }

        public void Add(string path)
        {
            if (string.IsNullOrEmpty(BasePath))
            {
                throw new InvalidOperationException("BasePath not set.");
            }

            if (!TryRemoveBasePath(BasePath, path, out var result))
            {
                throw new ArgumentException("BasePath not match!");
            }

            InternalAdd(result, path);
        }

        public void Add(string fileName, string filePath)
        {
            InternalAdd(fileName, filePath);
        }

        private void InternalAdd(string fileName, string filePath)
        {
            var node = new FileNode(fileName, filePath);
            if (node.FileName == null || node.FileName.Length == 0)
            {
                throw new ArgumentException("FileName can not be null or empty");
            }
            _nodes.Add(fileName, node);
            _count++;
        }

        public void Clear()
        {
            _nodes.Clear();
            _count = 0;
        }

        public void Save(string destinationPath)
        {
            using (var fs = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, false))
            {
                Save(fs, 0);
            }
        }
        public async Task SaveAsync(string destinationPath)
        {
            using (var fs = new FileStream(destinationPath, FileMode.CreateNew, FileAccess.Write, FileShare.None, 4096, false))
            {
                await SaveAsync(fs, 0).ConfigureAwait(false);
            }
        }
        public void Save(Stream stream, int offset = 0)
        {
            if (!BitConverter.IsLittleEndian)
            {
                throw new PlatformNotSupportedException();
            }

            // 获取所有文件节点
            var fileNodes = _nodes.Select(kv => kv.Value).ToArray();
            Array.Sort(fileNodes, FileNode.DefaultComparer);

            // 创建树结构
            var root = BTree.FromSortedArray<FileNode, IndexNodeData>(fileNodes, MaxItemCountPerNode);

            // 扁平化树结构
            var list = new List<BTreeNode<FileNode, IndexNodeData>>();
            int currentOffset = 0;
            list.Add(root);
            for (int i = 0; i < list.Count; i++)
            {
                var node = list[i];
                int size = UpdateIndexOffset(ref currentOffset, node);
                // 计算Padding
                int padding = CalcPadding(size, IndexNodePadding);
                node.NodeData.CalculatedPadding = padding;
                currentOffset += padding;

                // 加入子节点
                for (int j = 0; j < node.Count; j++)
                {
                    var subNode = node.LeftNodes[j];
                    if (subNode != null)
                        list.Add(subNode);
                }
                var rightNode = node.RightNode;
                if (rightNode != null)
                    list.Add(rightNode);
            }

            // 获取文件基本信息（文件大小）
            long fileOffset = 4096 + currentOffset;
            foreach (var node in fileNodes)
            {
                var fileInfo = new FileInfo(node.FilePath);
                if (!fileInfo.Exists)
                {
                    throw new FileNotFoundException(fileInfo.FullName);
                }

                node.FileOffset = fileOffset;
                node.FileSize = fileInfo.Length;
                if (UseIntFileSize && fileInfo.Length > uint.MaxValue)
                {
                    throw new InvalidDataException(string.Format("{0} file size too big.", fileInfo.FullName));
                }

                long size = GetFileNodeSize(node) + GetFileNodePropertiesSize(node);
                fileOffset += size;
                // 计算Padding
                int padding = CalcPadding(size, FileNodePadding);
                node.Padding = padding;
                fileOffset += padding;
            }

            // 写入文件头部和索引表
            var emptyBuffer = new byte[4096];
            const int InitialBufferSize = 16384;
            const int BufferFlushThreshold = InitialBufferSize;
            using (var ms = new MemoryStream(InitialBufferSize))
            using (var bw = new BinaryWriter(ms))
            {
                ms.WriteByte((byte)'I');
                ms.WriteByte((byte)'F');
                ms.WriteByte((byte)'P');
                ms.WriteByte((byte)'1');

                // flags
                ms.WriteByte((byte)(UseIntFileSize ? 1 : 0));
                ms.WriteByte(0);
                ms.WriteByte(0);
                ms.WriteByte(0);

                // reserved
                ms.WriteByte(0);
                ms.WriteByte(0);

                // MaxItemCountPerNode
                bw.Write(MaxItemCountPerNode);

                // headers-length
                bw.Write(4096 - _FileHeaderSize);
                // headers-padding
                ms.Write(emptyBuffer, 0, 4096 - _FileHeaderSize);

                for (int index = 0; index < list.Count; index++)
                {
                    var node = list[index];
                    var nextNode = (index == list.Count - 1) ? null : list[index + 1];

                    // count
                    bw.Write((ushort)node.Count);
                    bw.Write((ushort)0);
                    // left-child-index-offset
                    for (int i = 0; i < node.Count; i++)
                    {
                        bw.Write(node.LeftNodes[i]?.NodeData.IndexOffset ?? 0);
                    }
                    // right-child-index-offset
                    bw.Write(node.RightNode?.NodeData.IndexOffset ?? 0);
                    // file-hash
                    for (int i = 0; i < node.Count; i++)
                    {
                        bw.Write(node.Items[i].CalcFileNameHash());
                    }
                    // file-offset
                    for (int i = 0; i < node.Count; i++)
                    {
                        bw.Write(offset + node.Items[i].FileOffset);
                    }
                    // file-name-length
                    for (int i = 0; i < node.Count; i++)
                    {
                        bw.Write((ushort)node.Items[i].FileName.Length);
                    }
                    // next-node-offset
                    bw.Write(nextNode?.NodeData.IndexOffset ?? 0);
                    // file-names
                    for (int i = 0; i < node.Count; i++)
                    {
                        ms.Write(node.Items[i].FileName, 0, node.Items[i].FileName.Length);
                    }
                    int padding = node.NodeData.CalculatedPadding;
                    while (padding >= 4096)
                    {
                        ms.Write(emptyBuffer, 0, 4096);
                        padding -= 4096;
                    }
                    if (padding > 0)
                    {
                        ms.Write(emptyBuffer, 0, padding);
                    }

                    if (ms.Length > BufferFlushThreshold)
                    {
                        ms.Position = 0;
                        ms.CopyTo(stream);
                        ms.SetLength(0);
                    }
                }

                if (ms.Position > 0)
                {
                    ms.Position = 0;
                    ms.CopyTo(stream);
                }
            }

            // 写入文件内容
            using (var bw = new BinaryWriter(stream))
            {
                foreach (var node in fileNodes)
                {
                    if (UseIntFileSize)
                    {
                        bw.Write((uint)node.FileSize);
                    }
                    else
                    {
                        bw.Write(node.FileSize);
                    }

                    FileInfo fileInfo = new FileInfo(node.FilePath);
                    using (var fs = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, false))
                    {
                        fs.CopyTo(stream);
                    }
                    if (fileInfo.Length != node.FileSize)
                    {
                        throw new IOException("Incorrect copy count.");
                    }
                    if (node.WriteLastModifiedTimeUtc)
                    {
                        stream.WriteByte(1);
                        stream.WriteByte((byte)'L');
                        bw.Write((ushort)8);
                        bw.Write(fileInfo.LastWriteTimeUtc.Ticks);
                    }
                    stream.WriteByte(0);

                    long size = GetFileNodeSize(node) + GetFileNodePropertiesSize(node);
                    int padding = CalcPadding(size, FileNodePadding);
                    while (padding >= 1024)
                    {
                        stream.Write(emptyBuffer, 0, 1024);
                        padding -= 1024;
                    }
                    if (padding > 0)
                    {
                        stream.Write(emptyBuffer, 0, padding);
                    }
                }
            }
        }
        public async Task SaveAsync(Stream stream, int offset = 0)
        {
            if (!BitConverter.IsLittleEndian)
            {
                throw new PlatformNotSupportedException();
            }

            // 获取所有文件节点
            var fileNodes = _nodes.Select(kv => kv.Value).ToArray();
            Array.Sort(fileNodes, FileNode.DefaultComparer);

            // 创建树结构
            var root = BTree.FromSortedArray<FileNode, IndexNodeData>(fileNodes, MaxItemCountPerNode);

            // 扁平化树结构
            var list = new List<BTreeNode<FileNode, IndexNodeData>>();
            int currentOffset = 0;
            list.Add(root);
            for (int i = 0; i < list.Count; i++)
            {
                var node = list[i];
                int size = UpdateIndexOffset(ref currentOffset, node);
                // 计算Padding
                int padding = CalcPadding(size, IndexNodePadding);
                node.NodeData.CalculatedPadding = padding;
                currentOffset += padding;

                // 加入子节点
                for (int j = 0; j < node.Count; j++)
                {
                    var subNode = node.LeftNodes[j];
                    if (subNode != null)
                        list.Add(subNode);
                }
                var rightNode = node.RightNode;
                if (rightNode != null)
                    list.Add(rightNode);
            }

            // 获取文件基本信息（文件大小）
            long fileOffset = 4096 + currentOffset;
            foreach (var node in fileNodes)
            {
                var fileInfo = new FileInfo(node.FilePath);
                if (!fileInfo.Exists)
                {
                    throw new FileNotFoundException(fileInfo.FullName);
                }

                node.FileOffset = fileOffset;
                node.FileSize = fileInfo.Length;
                if (UseIntFileSize && fileInfo.Length > uint.MaxValue)
                {
                    throw new InvalidDataException(string.Format("{0} file size too big.", fileInfo.FullName));
                }

                long size = GetFileNodeSize(node) + GetFileNodePropertiesSize(node);
                fileOffset += size;
                // 计算Padding
                int padding = CalcPadding(size, FileNodePadding);
                node.Padding = padding;
                fileOffset += padding;
            }

            // 写入文件头部和索引表
            var emptyBuffer = new byte[4096];
            const int InitialBufferSize = 16384;
            const int BufferFlushThreshold = InitialBufferSize;
            using (var ms = new MemoryStream(InitialBufferSize))
            using (var bw = new BinaryWriter(ms))
            {
                ms.WriteByte((byte)'I');
                ms.WriteByte((byte)'F');
                ms.WriteByte((byte)'P');
                ms.WriteByte((byte)'1');

                // flags
                ms.WriteByte((byte)(UseIntFileSize ? 1 : 0));
                ms.WriteByte(0);
                ms.WriteByte(0);
                ms.WriteByte(0);

                // reserved
                ms.WriteByte(0);
                ms.WriteByte(0);

                // MaxItemCountPerNode
                bw.Write(MaxItemCountPerNode);

                // headers-length
                bw.Write(4096 - _FileHeaderSize);
                // headers-padding
                ms.Write(emptyBuffer, 0, 4096 - _FileHeaderSize);

                for (int index = 0; index < list.Count; index++)
                {
                    var node = list[index];
                    var nextNode = (index == list.Count - 1) ? null : list[index + 1];

                    // count
                    bw.Write((ushort)node.Count);
                    bw.Write((ushort)0);
                    // left-child-index-offset
                    for (int i = 0; i < node.Count; i++)
                    {
                        bw.Write(node.LeftNodes[i]?.NodeData.IndexOffset ?? 0);
                    }
                    // right-child-index-offset
                    bw.Write(node.RightNode?.NodeData.IndexOffset ?? 0);
                    // file-hash
                    for (int i = 0; i < node.Count; i++)
                    {
                        bw.Write(node.Items[i].CalcFileNameHash());
                    }
                    // file-offset
                    for (int i = 0; i < node.Count; i++)
                    {
                        bw.Write(offset + node.Items[i].FileOffset);
                    }
                    // file-name-length
                    for (int i = 0; i < node.Count; i++)
                    {
                        bw.Write((ushort)node.Items[i].FileName.Length);
                    }
                    // next-node-offset
                    bw.Write(nextNode?.NodeData.IndexOffset ?? 0);
                    // file-names
                    for (int i = 0; i < node.Count; i++)
                    {
                        ms.Write(node.Items[i].FileName, 0, node.Items[i].FileName.Length);
                    }
                    int padding = node.NodeData.CalculatedPadding;
                    while (padding >= 4096)
                    {
                        ms.Write(emptyBuffer, 0, 4096);
                        padding -= 4096;
                    }
                    if (padding > 0)
                    {
                        ms.Write(emptyBuffer, 0, padding);
                    }

                    if (ms.Length > BufferFlushThreshold)
                    {
                        ms.Position = 0;
                        await ms.CopyToAsync(stream).ConfigureAwait(false);
                        ms.SetLength(0);
                    }
                }

                if (ms.Position > 0)
                {
                    ms.Position = 0;
                    await ms.CopyToAsync(stream).ConfigureAwait(false);
                }
            }

            // 写入文件内容
            using (var ms = new MemoryStream())
            using (var bw = new BinaryWriter(ms))
            {
                foreach (var node in fileNodes)
                {
                    if (UseIntFileSize)
                    {
                        bw.Write((uint)node.FileSize);
                    }
                    else
                    {
                        bw.Write(node.FileSize);
                    }
                    await ms.FlushToAsync(stream).ConfigureAwait(false);

                    FileInfo fileInfo = new FileInfo(node.FilePath);
                    using (var fs = new FileStream(fileInfo.FullName, FileMode.Open, FileAccess.Read, FileShare.Read, 4096, true))
                    {
                        await fs.CopyToAsync(stream).ConfigureAwait(false);
                    }
                    if (fileInfo.Length != node.FileSize)
                    {
                        throw new IOException("Incorrect copy count.");
                    }
                    if (node.WriteLastModifiedTimeUtc)
                    {
                        bw.Write((byte)1);
                        bw.Write((byte)'L');
                        bw.Write((ushort)8);
                        bw.Write(fileInfo.LastWriteTimeUtc.Ticks);
                    }
                    bw.Write((byte)0);
                    await ms.FlushToAsync(stream).ConfigureAwait(false);
                    
                    long size = GetFileNodeSize(node) + GetFileNodePropertiesSize(node);
                    int padding = CalcPadding(size, FileNodePadding);
                    while (padding >= 1024)
                    {
                        await stream.WriteAsync(emptyBuffer, 0, 1024).ConfigureAwait(false);
                        padding -= 1024;
                    }
                    if (padding > 0)
                    {
                        await stream.WriteAsync(emptyBuffer, 0, padding).ConfigureAwait(false);
                    }
                }
            }
        }

        private int UpdateIndexOffset(ref int currentOffset, BTreeNode<FileNode, IndexNodeData> node)
        {
            node.NodeData.IndexOffset = currentOffset;
            int size = node.NodeData.GetNodeSize(node);
            currentOffset += size;
            return size;
        }

        private int CalcPadding(long currentSize, int paddingSize)
        {
            return (int)(paddingSize - currentSize % paddingSize) % paddingSize;
        }

        // Mutable struct! Use with caution!
        private struct IndexNodeData
        {
            public int IndexOffset;
            public int CalculatedPadding;

            public int GetNodeSize<TAny>(BTreeNode<FileNode, TAny> treeNode)
            {
                int size = 18 * treeNode.Count + 12; // 4 + 4 * TreeNode.Count + 4 + 4 * TreeNode.Count + 8 * TreeNode.Count + 2 * TreeNode.Count + 4;
                for (int i = 0; i < treeNode.Count; i++)
                {
                    size += treeNode.Items[i].FileName.Length;
                }
                return size;
            }
        }

        private int GetFileNodePropertiesSize(FileNode node)
        {
            return (node.WriteLastModifiedTimeUtc ? 12 : 0) + 1;
        }
        private long GetFileNodeSize(FileNode node)
        {

            return (UseIntFileSize ? 4 : 8) + node.FileSize;
        }

        private class FileNode
        {
            private static readonly UTF8Encoding s_encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);

            public byte[] FileName { get; private set; }
            public string FilePath { get; private set; }
            public long FileOffset { get; set; }
            public long FileSize { get; set; }
            public bool WriteLastModifiedTimeUtc { get; set; }
            public int Padding { get; set; }

            public FileNode(byte[] fileName, string filePath)
            {
                FileName = fileName;
                FilePath = filePath;
                FileOffset = 0;
                FileSize = 0;
                WriteLastModifiedTimeUtc = true;
            }
            public FileNode(string fileName, string filePath) : this(s_encoding.GetBytes(fileName), filePath)
            {

            }

            private uint? _hash;
            public uint CalcFileNameHash()
            {
                if (_hash != null)
                {
                    return _hash.Value;
                }
                uint hash = StringHash.Fnv1a.Hash32(FileName);
                _hash = hash;
                return hash;
            }

            private static readonly FileNodeComparer s_comparer = new FileNodeComparer();
            public static FileNodeComparer DefaultComparer => s_comparer;
            public class FileNodeComparer : Comparer<FileNode>
            {
                public override int Compare(FileNode x, FileNode y)
                {
                    if (x == null)
                        return y == null ? 0 : -1;
                    if (y == null)
                        return 1;

                    if (x.CalcFileNameHash() > y.CalcFileNameHash())
                        return 1;
                    else if (x.CalcFileNameHash() < y.CalcFileNameHash())
                        return -1;

                    return FileNameHelper.CompareFileNameNoHash(x.FileName, y.FileName);
                }
            }
        }

        private static bool TryRemoveBasePath(string directoryPath, string filePath, out string result)
        {
            directoryPath = new DirectoryInfo(directoryPath).FullName;
            filePath = new FileInfo(filePath).FullName;

            string splitter = directoryPath.StartsWith("/") ? "/" : "\\";
            if (!directoryPath.EndsWith(splitter))
            {
                directoryPath = directoryPath + splitter;
            }

            if (!filePath.StartsWith(directoryPath))
            {
                result = null;
                return false;
            }

            result = filePath.Substring(directoryPath.Length);
            if (splitter == "\\")
            {
                result = result.Replace('\\', '/');
            }
            return true;
        }
    }
}
